/*
 * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.ir.expressions

import org.jetbrains.kotlin.CompilerVersionOfApiDeprecation
import org.jetbrains.kotlin.DeprecatedForRemovalCompilerApi
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrParameterKind
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrSymbolOwner
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.symbols.IrBindableSymbol
import org.jetbrains.kotlin.ir.symbols.IrSymbol
import org.jetbrains.kotlin.ir.symbols.impl.IrFakeOverrideSymbolBase
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.util.getShapeOfParameters
import org.jetbrains.kotlin.ir.util.render
import org.jetbrains.kotlin.ir.util.resolveFakeOverrideMaybeAbstractOrFail
import org.jetbrains.kotlin.ir.util.transformInPlace
import org.jetbrains.kotlin.ir.visitors.IrTransformer
import org.jetbrains.kotlin.ir.visitors.IrVisitor
import org.jetbrains.kotlin.utils.setSize

// This class is not autogenerated to for the sake refactoring IR parameters - see KT-68003.
// However, it must be kept in sync with [org.jetbrains.kotlin.ir.generator.IrTree.memberAccessExpression].
abstract class IrMemberAccessExpression<S : IrSymbol> : IrDeclarationReference() {
    abstract override val symbol: S

    abstract var origin: IrStatementOrigin?

    /**
     * A list of all value arguments.
     *
     * It corresponds 1 to 1 with [IrFunction.parameters], and therefore should have the same size.
     * `null` value usually means that the default value of the corresponding parameter will be used.
     */
    val arguments: ValueArgumentsList = ValueArgumentsList()

    // Those properties indicate the shape of `this.symbol.owner`. They are filled
    // when the element is created, and whenever `symbol` changes. They are used
    // to enable usage of old argument API, which embeds partial information of
    // target's shape, on top of new API, which doesn't.
    // There are two exceptions:
    // - If `symbol` is unbound, they represent the expected shape of the target.
    //   It should become actual when the symbol is bound.
    // - On calls to insert/removeDispatch/extensionReceiver, `targetHas*Receiver` is overridden.
    //   In that case, we assume the call knows better than the declarations itself on whether
    //   it has a particular receiver parameter or not. This is usually because the receiver
    //   has been added/removed to the declaration after the call to it has been created.
    //   In that situation it would not be possible to signal back to all calls that the shape
    //   was changed. Even more, it is possible that a receiver argument is added to a call
    //   slightly before the corresponding receiver parameter is added to a declaration.
    //   In order not to break such code, we assume the shape specified on call is,
    //   or will eventually be right.
    private var targetParametersShapeInitialized: Boolean = false
    internal var targetContextParameterCount: Int = 0
        private set
    internal var targetHasDispatchReceiver: Boolean = false
        private set
    internal var targetHasExtensionReceiver: Boolean = false
        private set
    private var targetRegularParameterCount: Int = 0

    internal fun initializeTargetShapeExplicitly(
        hasDispatchReceiver: Boolean,
        hasExtensionReceiver: Boolean,
        contextParameterCount: Int,
        regularParameterCount: Int,
    ) {
        if (targetParametersShapeInitialized) {
            require(hasDispatchReceiver == targetHasDispatchReceiver)
            { "New symbol has different shape w.r.t. dispatch receiver" }
            require(hasExtensionReceiver == targetHasExtensionReceiver)
            { "New symbol has different shape w.r.t. extension receiver" }
            require(regularParameterCount + contextParameterCount == targetRegularParameterCount + targetContextParameterCount)
            { "New symbol has different shape w.r.t. value parameter count" }
        } else {
            val newAllParametersCount =
                (if (hasDispatchReceiver) 1 else 0) +
                        contextParameterCount +
                        (if (hasExtensionReceiver) 1 else 0) +
                        regularParameterCount
            repeat((newAllParametersCount - arguments.size).coerceAtLeast(0)) {
                arguments.add(null)
            }

            targetHasDispatchReceiver = hasDispatchReceiver
            targetHasExtensionReceiver = hasExtensionReceiver
            targetContextParameterCount = contextParameterCount
            targetRegularParameterCount = regularParameterCount
            targetParametersShapeInitialized = true
        }
    }

    fun initializeTargetShapeFromSymbol() {
        @Suppress("UNCHECKED_CAST")
        val target = (symbol as IrBindableSymbol<*, IrSymbolOwner>).getRealOwner()
        when (target) {
            is IrFunction -> {
                val targetShape = target.getShapeOfParameters()
                initializeTargetShapeExplicitly(
                    targetShape.hasDispatchReceiver,
                    targetShape.hasExtensionReceiver,
                    targetShape.contextParameterCount,
                    targetShape.regularParameterCount,
                )
            }
            is IrProperty -> {
                val hasDispatchReceiver: Boolean
                val hasExtensionReceiver: Boolean

                val accessor = when (this) {
                    is IrPropertyReference -> (getter ?: setter)?.getRealOwner()
                    is IrLocalDelegatedPropertyReference -> getter.owner
                    else -> error("Unexpected reference to a property from $this")
                }
                @OptIn(DeprecatedForRemovalCompilerApi::class)
                if (accessor != null) {
                    hasDispatchReceiver = accessor.dispatchReceiverParameter != null
                    hasExtensionReceiver = accessor.extensionReceiverParameter != null
                } else {
                    val realProperty = target.resolveFakeOverrideMaybeAbstractOrFail()
                    hasDispatchReceiver = !realProperty.backingField!!.isStatic
                    hasExtensionReceiver = false
                }

                initializeTargetShapeExplicitly(
                    hasDispatchReceiver,
                    hasExtensionReceiver,
                    0,
                    0,
                )
            }
        }
    }

    protected fun updateTargetSymbol() {
        initializeTargetShapeFromSymbol()
    }

    private fun ensureTargetShapeInitialized() {
        if (!targetParametersShapeInitialized) {
            initializeTargetShapeFromSymbol()
        }
    }

    internal fun rawCopyValueArgumentsFrom(other: IrMemberAccessExpression<*>) {
        arguments.clear()
        arguments.setSize(other.arguments.size)
        targetHasDispatchReceiver = other.targetHasDispatchReceiver
        targetHasExtensionReceiver = other.targetHasExtensionReceiver
        targetContextParameterCount = other.targetContextParameterCount
        targetRegularParameterCount = other.targetRegularParameterCount
        targetParametersShapeInitialized = other.targetParametersShapeInitialized
    }

    private fun <S : IrBindableSymbol<*, D>, D : IrSymbolOwner> S.getRealOwner(): D {
        var symbol = this
        while (symbol is IrFakeOverrideSymbolBase<*, *, *>) {
            @Suppress("UNCHECKED_CAST")
            symbol = symbol.originalSymbol as S
        }
        return symbol.owner
    }

    /**
     * Number of those arguments that correspond to [IrParameterKind.Context] and [IrParameterKind.Regular] parameters.
     *
     * ##### This is a deprecated API!
     * - If you want a number of _all_ arguments (which you should most of the time), use [arguments.size] instead.
     * - If you want a number of a particular [IrParameterKind] of parameters, you have to reach out to the corresponding function.
     *
     * A drop-in replacement (discouraged):
     * ```
     * symbol.owner.parameters.count { it.kind == IrParameterKind.Regular || it.kind == IrParameterKind.Context }
     * ```
     *
     * See docs/backend/IR_parameter_api_migration.md
     */
    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20)
    val valueArgumentsCount: Int
        get() {
            ensureTargetShapeInitialized()
            return targetRegularParameterCount + targetContextParameterCount
        }

    /**
     * Argument corresponding to the [IrParameterKind.DispatchReceiver] parameter, if any.
     *
     * When assigning a value other than `null`, it is `require`d that the callee has a corresponding dispatch receiver parameter.
     *
     * ##### Note on usage
     * Please try to use [arguments] instead, unless usage of [dispatchReceiver] makes for a cleaner/simpler code.
     *
     *  For example, the given pseudocode:
     *  ```kotlin
     *  if (call.symbol.owner.dispatchReceiverParameter != null)
     *      gen.invokevirtual(call.arguments[0])
     *  else
     *      gen.invokestatic()
     *  ```
     *  can be written more concisely:
     *  ```kotlin
     *  call.dispatchReceiver
     *      ?.let { gen.invokevirtual(it) }
     *      ?: gen.invokestatic()
     *  ```
     *
     *  Counter example: when dealing with calls of known shape, please just use [arguments] with hardcoded indices:
     *  ```kotlin
     *  call.arguments[0] = ...
     *  call.arguments[1] = ...
     *  ```
     *  instead of:
     *  ```kotlin
     *  call.dispatchReceiver = ...
     *  call.arguments[1] = ...
     *  ```
     *
     * In future, if we evaluate [dispatchReceiver] is not really useful (provides little win over just reaching to
     * function.dispatchReceiverParameter and then using [arguments]), it will be deprecated.
     *
     * See docs/backend/IR_parameter_api_migration.md
     */
    var dispatchReceiver: IrExpression?
        get() {
            ensureTargetShapeInitialized()
            return if (targetHasDispatchReceiver) {
                arguments[0]
            } else {
                null
            }
        }
        set(value) {
            ensureTargetShapeInitialized()
            if (targetHasDispatchReceiver) {
                arguments[0] = value
            } else {
                require(value == null) {
                    "${this.javaClass.simpleName} has no argument slot for the corresponding dispatch receiver parameter. " +
                            "If you are sure you want to add it (most likely for when the parameter has been added to the function since), " +
                            "use insertDispatchReceiver() instead."
                }
            }
        }

    /**
     * Argument corresponding to the [IrParameterKind.ExtensionReceiver] parameter, if any.
     *
     * ##### This is a deprecated API!
     * Only use [arguments] instead. If you need to know the meaning of the arguments, reach out to the corresponding function's parameters.
     * A drop-in replacement (discouraged):
     * ```
     * arguments[symbol.owner.parameters.indexOfFirst { it.kind == IrParameterKind.ExtensionReceiver }]
     * ```
     *
     * See docs/backend/IR_parameter_api_migration.md
     */
    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20)
    var extensionReceiver: IrExpression?
        get() {
            ensureTargetShapeInitialized()
            return if (targetHasExtensionReceiver) {
                val index = getExtensionReceiverIndex()
                arguments[index]
            } else {
                null
            }
        }
        set(value) {
            ensureTargetShapeInitialized()
            if (targetHasExtensionReceiver) {
                arguments[getExtensionReceiverIndex()] = value
            } else {
                require(value == null) {
                    "${this.javaClass.simpleName} has no argument slot for the corresponding extension receiver parameter. " +
                            "If you are sure you want to add it (most likely for when the parameter has been added to the function since), " +
                            "use insertExtensionReceiver() instead."
                }
            }
        }

    private fun getExtensionReceiverIndex(): Int {
        return (if (targetHasDispatchReceiver) 1 else 0) + targetContextParameterCount
    }


    /**
     * Methods below should be used to add/remove a receiver argument in correspondence to target function/property,
     * whose parameters changed _after_ this [IrMemberAccessExpression] was created.
     *
     * It can happen, e.g., in a lowering phase that modifies an existing function in-place by adding a `dispatchReceiverParameter`,
     * then adjusts all calls to that function by setting the corresponding `dispatchReceiver` argument.
     * In that case, you need to call [insertDispatchReceiver] method rather setting the argument directly via `dispatchReceiver` property.
     *
     * In practice, it is expected to be a very rare situation.
     *
     * #### Reasoning
     *
     * [KT-70054](https://youtrack.jetbrains.com/issue/KT-70054) introduces a single [arguments] list, that also
     * includes dispatch and extension receivers.
     * The existing (old) API will be implemented on top of that list for the period of migration. Then:
     * - `dispatchReceiver = ...` means `arguments[0] = ...`
     * - `insertDispatchReceiver(...)` means `arguments.add(0, ...)`
     * - `removeDispatchReceiver(...)` means `arguments.removeAt(0)`
     *
     * We need a way to distinguish those two cases so that [arguments] list stays intact with the corresponding [IrFunction.parameters] list
     * (i.e., so that they have the same sizes).
     *
     * Note: before the migration is finished you still need to use those methods, rather than `arguments.add()` / `arguments.removeAt()`
     * directly, in order for the old API to continue working.
     */

    /**
     * Adds a dispatch receiver to [IrMemberAccessExpression], whose target function/property has gotten
     * the corresponding dispatch receiver parameter some time _after_ the [IrMemberAccessExpression] was created.
     *
     * For details see the comments above.
     */
    fun insertDispatchReceiver(value: IrExpression?) {
        ensureTargetShapeInitialized()
        if (targetHasDispatchReceiver) {
            arguments[0] = value
        } else {
            arguments.add(0, value)
            targetHasDispatchReceiver = true
        }
    }

    /**
     * Removes a dispatch receiver from [IrMemberAccessExpression], whose target function/property has lost
     * the corresponding dispatch receiver parameter some time _after_ the [IrMemberAccessExpression] was created.
     *
     * For details see the comments above.
     */
    fun removeDispatchReceiver() {
        ensureTargetShapeInitialized()
        if (targetHasDispatchReceiver) {
            arguments.removeAt(0)
            targetHasDispatchReceiver = false
        }
    }

    /**
     * Adds an extension receiver to [IrMemberAccessExpression], whose target function/property has gotten
     * the corresponding extension receiver parameter some time _after_ the [IrMemberAccessExpression] was created.
     *
     * For details see the comments above.
     */
    fun insertExtensionReceiver(value: IrExpression?) {
        ensureTargetShapeInitialized()
        val index = getExtensionReceiverIndex()
        if (targetHasExtensionReceiver) {
            arguments[index] = value
        } else {
            arguments.add(index, value)
            targetHasExtensionReceiver = true
        }
    }

    /**
     * Removes an extension receiver from [IrMemberAccessExpression], whose target function/property has lost
     * the corresponding extension receiver parameter some time _after_ the [IrMemberAccessExpression] was created.
     *
     * For details see the comments above.
     */
    fun removeExtensionReceiver() {
        ensureTargetShapeInitialized()
        if (targetHasExtensionReceiver) {
            arguments.removeAt(getExtensionReceiverIndex())
            targetHasExtensionReceiver = false
        }
    }


    /**
     * Gets one of the arguments that correspond to either [IrParameterKind.Context] or [IrParameterKind.Regular] parameters.
     *
     * This is, [index] corresponds to [IrFunction.valueParameters] list, and not [IrFunction.parameters], which also includes
     * receiver parameters.
     *
     * ##### This is a deprecated API!
     * Use [arguments] instead.
     *
     * E.g. for code
     * ```
     * call.getValueArgument(parameter.indexInOldValueParameters)
     * ```
     *
     * the replacement should be
     * ```
     * call.arguments[parameter.indexInParameters]
     * ```
     * If you need to know the [IrParameterKind] of the argument, reach out to the corresponding function's parameter.
     *
     * See docs/backend/IR_parameter_api_migration.md
     */
    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20)
    fun getValueArgument(index: Int): IrExpression? {
        ensureTargetShapeInitialized()
        val actualIndex = getRealValueArgumentIndex(index)
        checkArgumentSlotAccess("value", actualIndex, this.arguments.size)
        return this.arguments[actualIndex]
    }

    /**
     * Sets one of arguments that correspond to [IrParameterKind.Context] or [IrParameterKind.Regular] parameters.
     *
     * This is, [index] corresponds to [IrFunction.valueParameters] list, and not [IrFunction.parameters], which also includes
     * receiver parameters.
     *
     * ##### This is a deprecated API!
     * Use [arguments] instead.
     *
     * E.g. for code
     * ```
     * call.putValueArgument(parameter.indexInOldValueParameters, ...)
     * ```
     *
     * the replacement should be
     * ```
     * call.arguments[parameter.indexInParameters] = ...
     * ```
     * If you need to know the [IrParameterKind] of the arguments, reach out to the corresponding function's parameter.
     *
     * See docs/backend/IR_parameter_api_migration.md
     */
    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20)
    fun putValueArgument(index: Int, valueArgument: IrExpression?) {
        ensureTargetShapeInitialized()
        val actualIndex = getRealValueArgumentIndex(index)
        checkArgumentSlotAccess("value", actualIndex, this.arguments.size)
        this.arguments[actualIndex] = valueArgument
    }

    private fun getRealValueArgumentIndex(index: Int): Int =
        (if (targetHasDispatchReceiver) 1 else 0) +
                (if (targetHasExtensionReceiver && index >= targetContextParameterCount) 1 else 0) +
                index


    abstract val typeArguments: MutableList<IrType?>

    internal fun initializeEmptyTypeArguments(count: Int) {
        repeat((count - typeArguments.size).coerceAtLeast(0)) {
            typeArguments.add(null)
        }
    }

    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20, replaceWith = "typeArguments.size")
    val typeArgumentsCount: Int
        get() = typeArguments.size

    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20, replaceWith = "typeArguments[index]")
    fun getTypeArgument(index: Int): IrType? {
        checkArgumentSlotAccess("type", index, typeArguments.size)
        return typeArguments[index]
    }

    @DeprecatedForRemovalCompilerApi(CompilerVersionOfApiDeprecation._2_1_20, replaceWith = "typeArguments[index] = value")
    fun putTypeArgument(index: Int, type: IrType?) {
        checkArgumentSlotAccess("type", index, typeArguments.size)
        typeArguments[index] = type
    }


    override fun <D> acceptChildren(visitor: IrVisitor<Unit, D>, data: D) {
        arguments.forEach { it?.accept(visitor, data) }
    }

    override fun <D> transformChildren(transformer: IrTransformer<D>, data: D) {
        arguments.transformInPlace(transformer, data)
    }


    inner class ValueArgumentsList : ArrayList<IrExpression?>() {
        operator fun get(parameter: IrValueParameter): IrExpression? {
            checkIndexingByParameter(parameter)
            return this[parameter.indexInParameters]
        }

        operator fun set(parameter: IrValueParameter, value: IrExpression?): IrExpression? {
            checkIndexingByParameter(parameter)
            return this.set(parameter.indexInParameters, value)
        }

        private fun checkIndexingByParameter(parameter: IrValueParameter) {
            require(parameter.parent == symbol.owner) {
                "Attempting to access argument corresponding to a parameter of different function.\n" +
                        "This IR element references ${symbol.owner.render()}, while asking about a parameter of ${parameter.parent.render()}"
            }
        }
    }
}